package database

import (
	"fmt"
	"github.com/DiracLee/dires-go/app/payload"
	"github.com/DiracLee/dires-go/cronjob"
	"github.com/DiracLee/dires-go/ds/dict"
	"github.com/DiracLee/dires-go/ds/list"
	"github.com/DiracLee/dires-go/ds/set"
	"github.com/DiracLee/dires-go/ds/sortedset"
	"github.com/DiracLee/dires-go/logger"
	"github.com/DiracLee/dires-go/syncx"
	"time"
)

type diresDB struct {
	index       int
	data        dict.Dict
	keysTTL     dict.Dict
	keysVersion dict.Dict
	addAOF      func([][]byte)
	syncx.LockBucket
	stopWorld syncx.WaitGroup
}

func (db *diresDB) AddAOF(cmdLine [][]byte) {
	db.addAOF(cmdLine)
}

func (db *diresDB) SetAddAOF(addAOF func([][]byte)) {
	db.addAOF = addAOF
}

func (db *diresDB) SetIndex(index int) {
	db.index = index
}

func (db *diresDB) GetIndex() int {
	return db.index
}

func (db *diresDB) execWithLock(cmdLine [][]byte) payload.Payload {
	return payload.NewErrPayload("DB::execWithLock not implemented")
}

func (db *diresDB) Get(key string) (*DataEntity, bool) {
	db.stopWorld.Wait()

	row, ok := db.data.Get(key)
	if !ok {
		return nil, false
	}
	if db.IsExpired(key) {
		return nil, false
	}
	entity, ok := row.(*DataEntity)
	if !ok {
		return nil, false
	}
	return entity, true
}

func (db *diresDB) PutOrSet(key string, entity *DataEntity) int {
	db.stopWorld.Wait()
	return db.data.PutOrSet(key, entity)
}

func (db *diresDB) SetIfExists(key string, entity *DataEntity) int {
	db.stopWorld.Wait()
	return db.data.SetIfExists(key, entity)
}

func (db *diresDB) PutIfNotExists(key string, entity *DataEntity) int {
	db.stopWorld.Wait()
	return db.data.PutIfNotExists(key, entity)
}

func (db *diresDB) remove(key string) {
	db.stopWorld.Wait()
	db.data.Remove(key)
	db.keysTTL.Remove(key)
	cronjob.Cancel(key)
}

func (db *diresDB) SetKeyTTL(key string, ttl time.Time) {
	db.stopWorld.Wait()
	db.keysTTL.PutOrSet(key, ttl)
	cronjob.RegisterTimed(ttl, key, func() error {
		db.RLock(key)
		defer db.RUnlock(key)
		t, exists := db.keysTTL.Get(key)
		if !exists {
			logger.Warnf("[DB::SetKeyTTL] key(%v) not exist in keysTTL", key)
			return nil
		}
		expireTime, ok := t.(time.Time)
		if !ok {
			logger.Errorf("[DB::SetKeyTTL] value of keysTTL got by key(%v) is a not time type", key)
			return fmt.Errorf("[Storage::SetKeyTTL] value of keysTTL got by key(%v) is a not time type", key)
		}
		for time.Now().Before(expireTime) {
			logger.Warnf("[DB::SetKeyTTL] it's before key(%v)'s expire time, wait a while to run", key)
			time.Sleep(time.Millisecond)
		}
		db.remove(key)
		return nil
	})
}

func (db *diresDB) GetTTL(key string) (time.Time, bool) {
	raw, exists := db.keysTTL.Get(key)
	if !exists {
		return time.Time{}, false
	}
	ttl, ok := raw.(time.Time)
	if !ok {
		return time.Time{}, false
	}
	return ttl, true
}

func (db *diresDB) Delete(keys ...string) int {
	db.stopWorld.Wait()
	removed := 0
	for _, key := range keys {
		if _, exists := db.data.Get(key); exists {
			db.remove(key)
			removed++
		}
	}
	return removed
}

func (db *diresDB) Flush() {
	db.stopWorld.Add(1)
	defer db.stopWorld.Done()

	db.data.Clear()
	db.keysTTL.Clear()
	db.LockBucket = syncx.NewLockBucket(lockBucketSize)
}

func (db *diresDB) Persist(key string) {
	db.stopWorld.Wait()
	db.keysTTL.Remove(key)
	cronjob.Cancel(key)
}

func (db *diresDB) IsExpired(key string) bool {
	t, ok := db.keysTTL.Get(key)
	if !ok {
		logger.Warnf("[DB::IsExpired] key(%s) not exist in keysTTL", key)
		return false
	}
	expireTime, ok := t.(time.Time)
	if time.Now().After(expireTime) {
		db.Delete(key)
		return true
	}
	return false
}

func (db *diresDB) GetVersion(key string) int64 {
	version, exists := db.keysVersion.Get(key)
	if !exists {
		logger.Warnf("[DB::GetVersion] key(%s) not exist in keysVersion", key)
		return 0
	}
	return version.(int64)
}

func (db *diresDB) AddVersion(keys ...string) {
	for _, key := range keys {
		versionCode := db.GetVersion(key)
		db.keysVersion.PutOrSet(key, versionCode+1)
	}
}

func (db *diresDB) ForEachWithExpire(cb func(key string, data *DataEntity, expiration *time.Time) bool) {
	db.data.ForEach(func(key string, raw interface{}) bool {
		entity, ok := raw.(*DataEntity)
		if !ok {
			logger.Errorf("[DB::ForEachWithExpire] value of data got by key(%v) is a not DataEntity type", key)
			return true
		}
		t, exists := db.keysTTL.Get(key)
		if !exists {
			logger.Errorf("[DB::ForEachWithExpire] key(%s) not exist in keysTTL", key)
			return true
		}
		expireTime, ok := t.(time.Time)
		if !ok {
			logger.Errorf("[DB::ForEachWithExpire] value of keysTTL got by key(%v) is a not time type", key)
			return true
		}
		return cb(key, entity, &expireTime)
	})
}

func (db *diresDB) ForEach(consumer func(key string, val interface{}) bool) {
	db.data.ForEach(consumer)
}

func (db *diresDB) ExecWithLock(cmdLine [][]byte) payload.Payload {
	return payload.NewErrPayload("DB::ExecWithLock not implemented")
}

func (db *diresDB) GetAsSet(key string) (set.Set, payload.ErrorPayload) {
	return nil, payload.NewErrPayload("DB::GetAsSet not implemented")
}

func (db *diresDB) GetOrInitSet(key string) (set set.Set, inited bool, errPayload payload.ErrorPayload) {
	return nil, false, payload.NewErrPayload("DB::GetOrInitSet not implemented")
}

func (db *diresDB) GetAsDict(key string) (dict.Dict, payload.ErrorPayload) {
	entity, exists := db.Get(key)
	if !exists {
		return nil, nil
	}
	d, ok := entity.Data.(dict.Dict)
	if !ok {
		return nil, payload.NewWrongTypeErrPayload()
	}
	return d, nil
}

func (db *diresDB) GetOrInitDict(key string) (d dict.Dict, inited bool, errPayload payload.ErrorPayload) {
	d, errPayload = db.GetAsDict(key)
	if errPayload != nil {
		return nil, false, errPayload
	}
	inited = false
	if d == nil {
		d = dict.NewSample()
		db.PutOrSet(key, &DataEntity{
			Data: d,
		})
		inited = true
	}
	return d, inited, nil
}

func (db *diresDB) GetAsList(key string) (list.List, payload.ErrorPayload) {
	entity, ok := db.Get(key)
	if !ok {
		return nil, nil
	}
	l, ok := entity.Data.(list.List)
	if !ok {
		return nil, payload.NewWrongTypeErrPayload()
	}
	return l, nil
}

func (db *diresDB) GetOrInitList(key string) (l list.List, isNew bool, errPayload payload.ErrorPayload) {
	l, errPayload = db.GetAsList(key)
	if errPayload != nil {
		return nil, false, errPayload
	}
	isNew = false
	if l == nil {
		l = list.New()
		db.PutOrSet(key, NewDataEntity(l))
		isNew = true
	}
	return l, isNew, nil
}

func (db *diresDB) GetAsSortedSet(key string) (*sortedset.SortedSet, payload.ErrorPayload) {
	return nil, payload.NewErrPayload("DB::GetAsSortedSet not implemented")
}

func (db *diresDB) GetOrInitSortedSet(key string) (sortedSet *sortedset.SortedSet, inited bool, errPayload payload.ErrorPayload) {
	return nil, false, payload.NewErrPayload("DB::GetOrInitSortedSet not implemented")
}

func (db *diresDB) GetAsString(key string) ([]byte, payload.ErrorPayload) {
	entity, ok := db.Get(key)
	if !ok {
		return nil, nil
	}
	bytes, ok := entity.Data.([]byte)
	if !ok {
		return nil, &payload.WrongTypeErrPayload{}
	}
	return bytes, nil
}
